/*
  FirmataParser.cpp
  Copyright (c) 2006-2008 Hans-Christoph Steiner.  All rights reserved.
  Copyright (C) 2009-2016 Jeff Hoefs.  All rights reserved.

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  See file LICENSE.txt for further informations on licensing terms.
*/

//******************************************************************************
//* Includes
//******************************************************************************

#include "FirmataParser.h"

#include "FirmataConstants.h"

using namespace firmata;

//******************************************************************************
//* Constructors
//******************************************************************************

/**
 * The FirmataParser class.
 * @param dataBuffer A pointer to an external buffer used to store parsed data
 * @param dataBufferSize The size of the external buffer
 */
FirmataParser::FirmataParser(uint8_t *const dataBuffer, size_t dataBufferSize)
    :
    dataBuffer(dataBuffer),
    dataBufferSize(dataBufferSize),
    executeMultiByteCommand(0),
    multiByteChannel(0),
    waitForData(0),
    parsingSysex(false),
    sysexBytesRead(0),
    currentAnalogCallbackContext((void *)NULL),
    currentDigitalCallbackContext((void *)NULL),
    currentReportAnalogCallbackContext((void *)NULL),
    currentReportDigitalCallbackContext((void *)NULL),
    currentPinModeCallbackContext((void *)NULL),
    currentPinValueCallbackContext((void *)NULL),
    currentReportFirmwareCallbackContext((void *)NULL),
    currentReportVersionCallbackContext((void *)NULL),
    currentDataBufferOverflowCallbackContext((void *)NULL),
    currentStringCallbackContext((void *)NULL),
    currentSysexCallbackContext((void *)NULL),
    currentSystemResetCallbackContext((void *)NULL),
    currentAnalogCallback((callbackFunction)NULL),
    currentDigitalCallback((callbackFunction)NULL),
    currentReportAnalogCallback((callbackFunction)NULL),
    currentReportDigitalCallback((callbackFunction)NULL),
    currentPinModeCallback((callbackFunction)NULL),
    currentPinValueCallback((callbackFunction)NULL),
    currentDataBufferOverflowCallback((dataBufferOverflowCallbackFunction)NULL),
    currentStringCallback((stringCallbackFunction)NULL),
    currentSysexCallback((sysexCallbackFunction)NULL),
    currentReportFirmwareCallback((versionCallbackFunction)NULL),
    currentReportVersionCallback((systemCallbackFunction)NULL),
    currentSystemResetCallback((systemCallbackFunction)NULL)
{
    allowBufferUpdate = ((uint8_t *)NULL == dataBuffer);
}

//******************************************************************************
//* Public Methods
//******************************************************************************

//------------------------------------------------------------------------------
// Serial Receive Handling

/**
 * Parse data from the input stream.
 * @param inputData A single byte to be added to the parser.
 */
void FirmataParser::parse(uint8_t inputData)
{
    uint8_t command;

    if (parsingSysex)
    {
        if (inputData == END_SYSEX)
        {
            //stop sysex byte
            parsingSysex = false;
            //fire off handler function
            processSysexMessage();
        }
        else
        {
            //normal data byte - add to buffer
            bufferDataAtPosition(inputData, sysexBytesRead);
            ++sysexBytesRead;
        }
    }
    else if ( (waitForData > 0) && (inputData < 128) )
    {
        --waitForData;
        bufferDataAtPosition(inputData, waitForData);
        if ( (waitForData == 0) && executeMultiByteCommand )   // got the whole message
        {
            switch (executeMultiByteCommand)
            {
            case ANALOG_MESSAGE:
                if (currentAnalogCallback)
                {
                    (*currentAnalogCallback)(currentAnalogCallbackContext,
                                             multiByteChannel,
                                             (dataBuffer[0] << 7)
                                             + dataBuffer[1]);
                }
                break;
            case DIGITAL_MESSAGE:
                if (currentDigitalCallback)
                {
                    (*currentDigitalCallback)(currentDigitalCallbackContext,
                                              multiByteChannel,
                                              (dataBuffer[0] << 7)
                                              + dataBuffer[1]);
                }
                break;
            case SET_PIN_MODE:
                if (currentPinModeCallback)
                    (*currentPinModeCallback)(currentPinModeCallbackContext, dataBuffer[1], dataBuffer[0]);
                break;
            case SET_DIGITAL_PIN_VALUE:
                if (currentPinValueCallback)
                    (*currentPinValueCallback)(currentPinValueCallbackContext, dataBuffer[1], dataBuffer[0]);
                break;
            case REPORT_ANALOG:
                if (currentReportAnalogCallback)
                    (*currentReportAnalogCallback)(currentReportAnalogCallbackContext, multiByteChannel, dataBuffer[0]);
                break;
            case REPORT_DIGITAL:
                if (currentReportDigitalCallback)
                    (*currentReportDigitalCallback)(currentReportDigitalCallbackContext, multiByteChannel, dataBuffer[0]);
                break;
            }
            executeMultiByteCommand = 0;
        }
    }
    else
    {
        // remove channel info from command byte if less than 0xF0
        if (inputData < 0xF0)
        {
            command = inputData & 0xF0;
            multiByteChannel = inputData & 0x0F;
        }
        else
        {
            command = inputData;
            // commands in the 0xF* range don't use channel data
        }
        switch (command)
        {
        case ANALOG_MESSAGE:
        case DIGITAL_MESSAGE:
        case SET_PIN_MODE:
        case SET_DIGITAL_PIN_VALUE:
            waitForData = 2; // two data bytes needed
            executeMultiByteCommand = command;
            break;
        case REPORT_ANALOG:
        case REPORT_DIGITAL:
            waitForData = 1; // one data byte needed
            executeMultiByteCommand = command;
            break;
        case START_SYSEX:
            parsingSysex = true;
            sysexBytesRead = 0;
            break;
        case SYSTEM_RESET:
            systemReset();
            break;
        case REPORT_VERSION:
            if (currentReportVersionCallback)
                (*currentReportVersionCallback)(currentReportVersionCallbackContext);
            break;
        }
    }
}

/**
 * @return Returns true if the parser is actively parsing data.
 */
bool FirmataParser::isParsingMessage(void)
const
{
    return (waitForData > 0 || parsingSysex);
}

/**
 * Provides a mechanism to either set or update the working buffer of the parser.
 * The method will be enabled when no buffer has been provided, or an overflow
 * condition exists.
 * @param dataBuffer A pointer to an external buffer used to store parsed data
 * @param dataBufferSize The size of the external buffer
 */
int FirmataParser::setDataBufferOfSize(uint8_t *dataBuffer, size_t dataBufferSize)
{
    int result;

    if ( !allowBufferUpdate )
    {
        result = __LINE__;
    }
    else if ((uint8_t *)NULL == dataBuffer)
    {
        result = __LINE__;
    }
    else
    {
        this->dataBuffer = dataBuffer;
        this->dataBufferSize = dataBufferSize;
        allowBufferUpdate = false;
        result = 0;
    }

    return result;
}

/**
 * Attach a generic sysex callback function to a command (options are: ANALOG_MESSAGE,
 * DIGITAL_MESSAGE, REPORT_ANALOG, REPORT DIGITAL, SET_PIN_MODE and SET_DIGITAL_PIN_VALUE).
 * @param command The ID of the command to attach a callback function to.
 * @param newFunction A reference to the callback function to attach.
 * @param context An optional context to be provided to the callback function (NULL by default).
 * @note The context parameter is provided so you can pass a parameter, by reference, to
 *       your callback function.
 */
void FirmataParser::attach(uint8_t command, callbackFunction newFunction, void *context)
{
    switch (command)
    {
    case ANALOG_MESSAGE:
        currentAnalogCallback = newFunction;
        currentAnalogCallbackContext = context;
        break;
    case DIGITAL_MESSAGE:
        currentDigitalCallback = newFunction;
        currentDigitalCallbackContext = context;
        break;
    case REPORT_ANALOG:
        currentReportAnalogCallback = newFunction;
        currentReportAnalogCallbackContext = context;
        break;
    case REPORT_DIGITAL:
        currentReportDigitalCallback = newFunction;
        currentReportDigitalCallbackContext = context;
        break;
    case SET_PIN_MODE:
        currentPinModeCallback = newFunction;
        currentPinModeCallbackContext = context;
        break;
    case SET_DIGITAL_PIN_VALUE:
        currentPinValueCallback = newFunction;
        currentPinValueCallbackContext = context;
        break;
    }
}

/**
 * Attach a version callback function (supported option: REPORT_FIRMWARE).
 * @param command The ID of the command to attach a callback function to.
 * @param newFunction A reference to the callback function to attach.
 * @param context An optional context to be provided to the callback function (NULL by default).
 * @note The context parameter is provided so you can pass a parameter, by reference, to
 *       your callback function.
 */
void FirmataParser::attach(uint8_t command, versionCallbackFunction newFunction, void *context)
{
    switch (command)
    {
    case REPORT_FIRMWARE:
        currentReportFirmwareCallback = newFunction;
        currentReportFirmwareCallbackContext = context;
        break;
    }
}

/**
 * Attach a system callback function (supported options are: SYSTEM_RESET, REPORT_VERSION).
 * @param command The ID of the command to attach a callback function to.
 * @param newFunction A reference to the callback function to attach.
 * @param context An optional context to be provided to the callback function (NULL by default).
 * @note The context parameter is provided so you can pass a parameter, by reference, to
 *       your callback function.
 */
void FirmataParser::attach(uint8_t command, systemCallbackFunction newFunction, void *context)
{
    switch (command)
    {
    case REPORT_VERSION:
        currentReportVersionCallback = newFunction;
        currentReportVersionCallbackContext = context;
        break;
    case SYSTEM_RESET:
        currentSystemResetCallback = newFunction;
        currentSystemResetCallbackContext = context;
        break;
    }
}

/**
 * Attach a callback function for the STRING_DATA command.
 * @param command Must be set to STRING_DATA or it will be ignored.
 * @param newFunction A reference to the string callback function to attach.
 * @param context An optional context to be provided to the callback function (NULL by default).
 * @note The context parameter is provided so you can pass a parameter, by reference, to
 *       your callback function.
 */
void FirmataParser::attach(uint8_t command, stringCallbackFunction newFunction, void *context)
{
    switch (command)
    {
    case STRING_DATA:
        currentStringCallback = newFunction;
        currentStringCallbackContext = context;
        break;
    }
}

/**
 * Attach a generic sysex callback function to sysex command.
 * @param command The ID of the command to attach a callback function to.
 * @param newFunction A reference to the sysex callback function to attach.
 * @param context An optional context to be provided to the callback function (NULL by default).
 * @note The context parameter is provided so you can pass a parameter, by reference, to
 *       your callback function.
 */
void FirmataParser::attach(uint8_t command, sysexCallbackFunction newFunction, void *context)
{
    (void)command;
    currentSysexCallback = newFunction;
    currentSysexCallbackContext = context;
}

/**
 * Attach a buffer overflow callback
 * @param newFunction A reference to the buffer overflow callback function to attach.
 * @param context An optional context to be provided to the callback function (NULL by default).
 * @note The context parameter is provided so you can pass a parameter, by reference, to
 *       your callback function.
 */
void FirmataParser::attach(dataBufferOverflowCallbackFunction newFunction, void *context)
{
    currentDataBufferOverflowCallback = newFunction;
    currentDataBufferOverflowCallbackContext = context;
}

/**
 * Detach a callback function for a specified command (such as SYSTEM_RESET, STRING_DATA,
 * ANALOG_MESSAGE, DIGITAL_MESSAGE, etc).
 * @param command The ID of the command to detatch the callback function from.
 */
void FirmataParser::detach(uint8_t command)
{
    switch (command)
    {
    case REPORT_FIRMWARE:
        attach(command, (versionCallbackFunction)NULL, NULL);
        break;
    case REPORT_VERSION:
    case SYSTEM_RESET:
        attach(command, (systemCallbackFunction)NULL, NULL);
        break;
    case STRING_DATA:
        attach(command, (stringCallbackFunction)NULL, NULL);
        break;
    case START_SYSEX:
        attach(command, (sysexCallbackFunction)NULL, NULL);
        break;
    default:
        attach(command, (callbackFunction)NULL, NULL);
        break;
    }
}

/**
 * Detach the buffer overflow callback
 * @param <unused> Any pointer of type dataBufferOverflowCallbackFunction.
 */
void FirmataParser::detach(dataBufferOverflowCallbackFunction)
{
    currentDataBufferOverflowCallback = (dataBufferOverflowCallbackFunction)NULL;
    currentDataBufferOverflowCallbackContext = (void *)NULL;
}

//******************************************************************************
//* Private Methods
//******************************************************************************

/**
 * Buffer abstraction to prevent memory corruption
 * @param data The byte to put into the buffer
 * @param pos The position to insert the byte into the buffer
 * @return writeError A boolean to indicate if an error occured
 * @private
 */
bool FirmataParser::bufferDataAtPosition(const uint8_t data, const size_t pos)
{
    bool bufferOverflow = (pos >= dataBufferSize);

    // Notify of overflow condition
    if ( bufferOverflow
            && ((dataBufferOverflowCallbackFunction)NULL != currentDataBufferOverflowCallback) )
    {
        allowBufferUpdate = true;
        currentDataBufferOverflowCallback(currentDataBufferOverflowCallbackContext);
        // Check if overflow was resolved during callback
        bufferOverflow = (pos >= dataBufferSize);
    }

    // Write data to buffer if no overflow condition persist
    if ( !bufferOverflow )
    {
        dataBuffer[pos] = data;
    }

    return bufferOverflow;
}

/**
 * Transform 7-bit firmata message into 8-bit stream
 * @param bytec The encoded data byte length of the message (max: 16383).
 * @param bytev A pointer to the encoded array of data bytes.
 * @return The length of the decoded data.
 * @note The conversion will be done in place on the provided buffer.
 * @private
 */
size_t FirmataParser::decodeByteStream(size_t bytec, uint8_t *bytev)
{
    size_t decoded_bytes, i;

    for ( i = 0, decoded_bytes = 0 ; i < bytec ; ++decoded_bytes, ++i )
    {
        bytev[decoded_bytes] = bytev[i];
        bytev[decoded_bytes] |= (uint8_t)(bytev[++i] << 7);
    }

    return decoded_bytes;
}

/**
 * Process incoming sysex messages. Handles REPORT_FIRMWARE and STRING_DATA internally.
 * Calls callback function for STRING_DATA and all other sysex messages.
 * @private
 */
void FirmataParser::processSysexMessage(void)
{
    switch (dataBuffer[0])   //first byte in buffer is command
    {
    case REPORT_FIRMWARE:
        if (currentReportFirmwareCallback)
        {
            const size_t major_version_offset = 1;
            const size_t minor_version_offset = 2;
            const size_t string_offset = 3;
            // Test for malformed REPORT_FIRMWARE message (used to query firmware prior to Firmata v3.0.0)
            if ( 3 > sysexBytesRead )
            {
                (*currentReportFirmwareCallback)(currentReportFirmwareCallbackContext, 0, 0, (const char *)NULL);
            }
            else
            {
                const size_t end_of_string = (string_offset + decodeByteStream((sysexBytesRead - string_offset), &dataBuffer[string_offset]));
                bufferDataAtPosition('\0', end_of_string); // NULL terminate the string
                (*currentReportFirmwareCallback)(currentReportFirmwareCallbackContext, (size_t)dataBuffer[major_version_offset], (size_t)dataBuffer[minor_version_offset], (const char *)&dataBuffer[string_offset]);
            }
        }
        break;
    case STRING_DATA:
        if (currentStringCallback)
        {
            const size_t string_offset = 1;
            const size_t end_of_string = (string_offset + decodeByteStream((sysexBytesRead - string_offset), &dataBuffer[string_offset]));
            bufferDataAtPosition('\0', end_of_string); // NULL terminate the string
            (*currentStringCallback)(currentStringCallbackContext, (const char *)&dataBuffer[string_offset]);
        }
        break;
    default:
        if (currentSysexCallback)
            (*currentSysexCallback)(currentSysexCallbackContext, dataBuffer[0], sysexBytesRead - 1, dataBuffer + 1);
    }
}

/**
 * Resets the system state upon a SYSTEM_RESET message from the host software.
 * @private
 */
void FirmataParser::systemReset(void)
{
    size_t i;

    waitForData = 0; // this flag says the next serial input will be data
    executeMultiByteCommand = 0; // execute this after getting multi-byte data
    multiByteChannel = 0; // channel data for multiByteCommands

    for (i = 0; i < dataBufferSize; ++i)
    {
        dataBuffer[i] = 0;
    }

    parsingSysex = false;
    sysexBytesRead = 0;

    if (currentSystemResetCallback)
        (*currentSystemResetCallback)(currentSystemResetCallbackContext);
}
